package org.hepeng.fabric.gateway.contract.proxy;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.hepeng.fabric.contract.api.ContractProvider;
import org.hepeng.fabric.contract.api.annotation.ContractAPI;
import org.hepeng.fabric.contract.api.annotation.TxFunction;
import org.hepeng.fabric.gateway.event.NetworkCache;
import org.hepeng.fabric.gateway.pool.FabricGatewayObject;
import org.hyperledger.fabric.client.Contract;
import org.hyperledger.fabric.client.SubmittedTransaction;
import org.hyperledger.fabric.contract.Context;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * @author he peng
 * @date 2022/3/23
 */

public class ContractProxy implements InvocationHandler {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private final Class<?> contractInterface;

    private final ContractProvider contractProvider;

    private ContractProxy(Class<?> contractInterface, ContractProvider contractProvider) {
        this.contractInterface = contractInterface;
        this.contractProvider = contractProvider;
    }

    public static <T> T newProxy(Class<T> contractInterface , ContractProvider contractProvider) {

        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader() ,
                new Class[]{contractInterface},
                new ContractProxy(contractInterface , contractProvider));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        TxFunction txFunction = method.getDeclaredAnnotation(TxFunction.class);
        if (Objects.isNull(txFunction)) {
            return method.invoke(proxy , args);
        }

        Object result = null;

        ContractAPI contractAPIAtn = contractInterface.getDeclaredAnnotation(ContractAPI.class);
        Contract contract = contractProvider.getContract(contractAPIAtn);

        if (Objects.isNull(contract)) {
            throw new RuntimeException(String.format("The %s contract instance does not exist" , contractAPIAtn.name()));
        }

        String[] actualArgs = argConvert(txFunction, argOverride(method, args));

        try {
            if (Objects.equals(TxFunction.Type.QUERY , txFunction.type())) {
                result = invokeQuery(contract , method , argConvert(txFunction , actualArgs));
            } else if (Objects.equals(TxFunction.Type.SUBMIT , txFunction.type())) {
                result = invokeSubmitAsync(contract , method , actualArgs);
            }
        } finally {
            FabricGatewayObject fabricGatewayObject = FabricGatewayObjectHolder.remove();
            if (Objects.nonNull(fabricGatewayObject)) {
                String channel = fabricGatewayObject.getKey().getChannel();
                NetworkCache.put(contractAPIAtn.chainCode() , fabricGatewayObject.getNetwork(channel));
            }
        }


        return result;
    }

    private Object invokeQuery(Contract contract , Method method , String[] args) throws Exception {

        byte[] bytes = contract.newProposal(method.getName())
                .addArguments(args)
                .build()
                .evaluate();

        return OBJECT_MAPPER.readValue(new String(bytes, StandardCharsets.UTF_8) , method.getReturnType());
    }

    private Object invokeSubmitAsync(Contract contract , Method method , String[] args) throws Exception {

        SubmittedTransaction submittedTransaction = contract.newProposal(method.getName())
                .addArguments(args)
                .build()
                .endorse()
                .submitAsync();

        byte[] bytes = submittedTransaction.getResult();
        return OBJECT_MAPPER.readValue(new String(bytes, StandardCharsets.UTF_8), method.getReturnType());
    }

    private String [] argConvert(TxFunction txFunction , Object[] args) {
        return txFunction.argType().convert(args);
    }

    private Object[] argOverride(Method method , Object[] args) {

        Object[] newArgs = args;
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (int i = 0 ; i < parameterTypes.length ; i++) {
            if (parameterTypes[i].isAssignableFrom(Context.class)) {
                int length = args.length;
                newArgs = (Object[]) Array.newInstance(args.getClass().getComponentType(), length - 1);
                System.arraycopy(args, 0, newArgs, 0, i);
                if (i < length - 1) {
                    System.arraycopy(args, i + 1, newArgs, i, length - i - 1);
                }
            }
        }

        return newArgs;
    }
}